import type { AsyncComponentLoader, Component, VNode } from 'vue';
import { Suspense, defineAsyncComponent, defineComponent, h, onErrorCaptured, ref } from 'vue';
import type { ComponentProps } from 'vue-component-type-helpers';
import DefaultLoadingComponent from './Loading.vue';
import DefaultErrorComponent from './Error.vue';

interface DefineSuspenseOptions {
  loadingComponent?: Component;
  errorComponent?: Component;
  delay?: number;
  timeout?: number;
}

export function defineSuspense<T extends Component>(
  asyncComponentFactory: AsyncComponentLoader<T>,
  options: DefineSuspenseOptions = {},
): T {
  const {
    loadingComponent,
    errorComponent,
    delay = 200,
    timeout = 10 * 1000,
  } = options;

  const AsyncComp = defineAsyncComponent({
    loader: asyncComponentFactory,
    loadingComponent,
    errorComponent,
  });

  const SuspenseWrapper = defineComponent({
    name: 'SuspenseWrapper',
    setup(_, { attrs, slots }) {
      const error = ref<Error | null>(null);
      const key = ref(0);
      const showLoading = ref(false);
      const loadingDelayTimer = ref<number | null>(null);
      const timeoutTimer = ref<number | null>(null);

      onErrorCaptured((e: Error) => {
        error.value = e;
        return true;
      });

      const reset = () => {
        error.value = null;
        showLoading.value = false;

        if (loadingDelayTimer.value !== null)
          clearTimeout(loadingDelayTimer.value);

        if (timeoutTimer.value !== null)
          clearTimeout(timeoutTimer.value);
      };

      const startTimers = () => {
        loadingDelayTimer.value = window.setTimeout(() => {
          showLoading.value = true;
        }, delay);

        timeoutTimer.value = window.setTimeout(() => {
          if (!error.value)
            error.value = new Error('Loading timed out');
        }, timeout);
      };

      const retry = () => {
        key.value++;
        reset();
        startTimers();
      };

      startTimers();

      return (): VNode => {
        if (error.value) {
          return h(errorComponent || h(DefaultErrorComponent), { onClick: retry, errorMsg: error.value?.message }, {
            default: () => `Error: ${error.value?.message}. Click to retry.`,
          });
        }

        return h(Suspense, { key: key.value, onResolve: reset }, {
          default: () => h(AsyncComp, attrs, slots),
          fallback: () => showLoading.value
            ? h(loadingComponent || DefaultLoadingComponent)
            : h('div'),
        });
      };
    },
  });

  return SuspenseWrapper as unknown as T;
}

export function buildLoadingVNode(props?: ComponentProps<typeof DefaultLoadingComponent>) {
  return h(DefaultLoadingComponent, props);
}

export function buildErrorVNode(props?: ComponentProps<typeof DefaultErrorComponent>) {
  return h(DefaultErrorComponent, props);
}
